package com.shm.leetcode;

import java.util.*;

/**
 * @author: shm
 * @dateTime: 2020/10/5 17:26
 * @description: 18. 四数之和
 * 给定一个包含 n 个整数的数组 nums 和一个目标值 target，判断 nums 中是否存在四个元素 a，b，c 和 d ，使得 a + b + c + d 的值与 target 相等？找出所有满足条件且不重复的四元组。
 * <p>
 * 注意：
 * <p>
 * 答案中不可以包含重复的四元组。
 * <p>
 * 示例：
 * <p>
 * 给定数组 nums = [1, 0, -1, 0, -2, 2]，和 target = 0。
 * <p>
 * 满足要求的四元组集合为：
 * [
 * [-1,  0, 0, 1],
 * [-2, -1, 1, 2],
 * [-2,  0, 0, 2]
 * ]
 */
public class FourSum {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i < nums.length; i++) {
            int value = target - nums[i];
            List<Integer> temp = new ArrayList<>();
            temp.add(nums[i]);
            for (int j = i + 1; j < nums.length; j++) {
                if (j == i) {
                    continue;
                }
                value = value - nums[j];
                temp.add(nums[j]);
                for (int k = j + 1; k < nums.length; k++) {
                    if (j == k) {
                        continue;
                    }
                    value = value - nums[k];
                    temp.add(nums[k]);
                    for (int p = k + 1; p < nums.length; p++) {
                        if (p == k) {
                            continue;
                        }
                        value = value - nums[p];
                        temp.add(nums[k]);
                        if (value == 0) {
                            ans.add(temp);
                            break;
                        }
                    }
                }
            }
        }
        return ans;
    }


    Set<Integer> set = new HashSet<>();

    /**
     * @author: shm
     * @dateTime: 2020/10/5 19:13
     * @description: DFS 回溯大家都会，现在问题就是剪枝
     * 如果剪枝不够狠必超时
     * <p>
     * 首先将 nums 升序排序，并把答案四元组中没确定的个数设为 n
     * <p>
     * 我把剪枝分为了 4 类，括号内的是用什么完成剪枝
     * <p>
     * 如果剩余可选的数字数量少于 n，则剪掉（递归返回）；
     * 每层递归中，从第二轮循环开始，如果数组中当前数字和前一个相同，则剪掉（进行下一次循环，这条的任务就是去重）；
     * 如果 当前数字 + 已确定数字的和 + (n - 1) * 排序后数组中当前数字的下一个数字 > target，则说明后面的数无论怎么选，加起来都一定大于 target，所以剪掉（递归返回）；
     * 如果 当前数字 + 已确定数字的和 + (n - 1) * 排序后数组最后一个数字 < target，则说明后面的数无论怎么选，加起来都一定小于 target，所以剪掉（进行下一次循环）；
     * <p>
     * 作者：Provencih
     * 链接：https://leetcode-cn.com/problems/4sum/solution/mei-kan-dao-ji-ge-hui-su-de-ti-jie-na-wo-lai-xie-y/
     */
    public List<List<Integer>> fourSum_2(int[] nums, int target) {
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> temp = new ArrayList<>();
        Arrays.sort(nums);

        for (int num : nums) {
            set.add(num);
        }
        dfs(nums, target, ans, temp, 0);
        return ans;
    }

    public void dfs(int[] nums, int target, List<List<Integer>> ans, List<Integer> temp, int index) {
        //方法1
        if (temp.size() == 4 && target == 0) {
            ans.add(new ArrayList<>(temp));
            return;
        }
        //方法2、优化 最后一个数可以用哈希表来加速查找，会有速度提升
        if (temp.size() == 3) {
            if (set.contains(target) && target > nums[index - 1] || target == nums[index]) {
                temp.add(target);
                ans.add(new ArrayList<>(temp));
                temp.remove(temp.size() - 1);
            }
            return;
        }


        if (index == nums.length) {
            return;
        }
        for (int i = index; i < nums.length; i++) {
            //去重
            if (i > index && nums[i] == nums[i - 1]) {
                continue;
            }
            //如果剩余可选的数字数量少于 n，剪枝
            if (nums.length - i < 4 - temp.size()) {
                return;
            }
            //当前数字 + 已确定数字的和 + (n - 1) * 排序后数组中当前数字的下一个数字 > target,剪枝
            if (i < nums.length - 1 && nums[i] + (3 - temp.size()) * nums[i + 1] > target) {
                return;
            }
            //当前数字 + 已确定数字的和 + (n - 1) * 排序后数组最后一个数字 < target,剪枝
            if (i < nums.length - 1 && nums[i] + (3 - temp.size()) * nums[nums.length - 1] < target) {
                continue;
            }
            temp.add(nums[i]);
            dfs(nums, target - nums[i], ans, temp, i + 1);
            temp.remove(temp.size() - 1);
        }
    }


    /**
     * @author: shm
     * @dateTime: 2020/10/5 19:48
     * @description: 方法一：排序 + 双指针
     * 思路与算法
     * <p>
     * 最朴素的方法是使用四重循环枚举所有的四元组，然后使用哈希表进行去重操作，得到不包含重复四元组的最终答案。假设数组的长度是 nn，则该方法中，枚举的时间复杂度为 O(n^4)O(n
     * 4
     * )，去重操作的时间复杂度和空间复杂度也很高，因此需要换一种思路。
     * <p>
     * 为了避免枚举到重复四元组，则需要保证每一重循环枚举到的元素不小于其上一重循环枚举到的元素，且在同一重循环中不能多次枚举到相同的元素。
     * <p>
     * 为了实现上述要求，可以对数组进行排序，并且在循环过程中遵循以下两点：
     * <p>
     * 每一种循环枚举到的下标必须大于上一重循环枚举到的下标；
     * <p>
     * 同一重循环中，如果当前元素与上一个元素相同，则跳过当前元素。
     * <p>
     * 使用上述方法，可以避免枚举到重复四元组，但是由于仍使用四重循环，时间复杂度仍是 O(n^4)O(n
     * 4
     * )。注意到数组已经被排序，因此可以使用双指针的方法去掉一重循环。
     * <p>
     * 使用两重循环分别枚举前两个数，然后在两重循环枚举到的数之后使用双指针枚举剩下的两个数。假设两重循环枚举到的前两个数分别位于下标 ii 和 jj，其中 i<ji<j。初始时，左右指针分别指向下标 j+1j+1 和下标 n-1n−1。每次计算四个数的和，并进行如下操作：
     * <p>
     * 如果和等于 \textit{target}target，则将枚举到的四个数加到答案中，然后将左指针右移直到遇到不同的数，将右指针左移直到遇到不同的数；
     * <p>
     * 如果和小于 \textit{target}target，则将左指针右移一位；
     * <p>
     * 如果和大于 \textit{target}target，则将右指针左移一位。
     * <p>
     * 使用双指针枚举剩下的两个数的时间复杂度是 O(n)O(n)，因此总时间复杂度是 O(n^3)O(n
     * 3
     * )，低于 O(n^4)O(n
     * 4
     * )。
     * <p>
     * 具体实现时，还可以进行一些剪枝操作：
     * <p>
     * 在确定第一个数之后，如果 \textit{nums}[i]+\textit{nums}[i+1]+\textit{nums}[i+2]+\textit{nums}[i+3]>\textit{target}nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target，说明此时剩下的三个数无论取什么值，四数之和一定大于 \textit{target}target，因此退出第一重循环；
     * 在确定第一个数之后，如果 \textit{nums}[i]+\textit{nums}[n-3]+\textit{nums}[n-2]+\textit{nums}[n-1]<\textit{target}nums[i]+nums[n−3]+nums[n−2]+nums[n−1]<target，说明此时剩下的三个数无论取什么值，四数之和一定小于 \textit{target}target，因此第一重循环直接进入下一轮，枚举 \textit{nums}[i+1]nums[i+1]；
     * 在确定前两个数之后，如果 \textit{nums}[i]+\textit{nums}[j]+\textit{nums}[j+1]+\textit{nums}[j+2]>\textit{target}nums[i]+nums[j]+nums[j+1]+nums[j+2]>target，说明此时剩下的两个数无论取什么值，四数之和一定大于 \textit{target}target，因此退出第二重循环；
     * 在确定前两个数之后，如果 \textit{nums}[i]+\textit{nums}[j]+\textit{nums}[n-2]+\textit{nums}[n-1]<\textit{target}nums[i]+nums[j]+nums[n−2]+nums[n−1]<target，说明此时剩下的两个数无论取什么值，四数之和一定小于 \textit{target}target，因此第二重循环直接进入下一轮，枚举 \textit{nums}[j+1]nums[j+1]。
     * <p>
     * 复杂度分析
     * <p>
     * 时间复杂度：O(n^3)O(n
     * 3
     * )，其中 nn 是数组的长度。排序的时间复杂度是 O(n \log n)O(nlogn)，枚举四元组的时间复杂度是 O(n^3)O(n
     * 3
     * )，因此总时间复杂度为 O(n^3+n\log n)=O(n^3)O(n
     * 3
     * +nlogn)=O(n
     * 3
     * )。
     * <p>
     * 空间复杂度：O(\log n)O(logn)，其中 nn 是数组的长度。空间复杂度主要取决于排序额外使用的空间。此外排序修改了输入数组 \textit{nums}nums，实际情况中不一定允许，因此也可以看成使用了一个额外的数组存储了数组 \textit{nums}nums 的副本并排序，空间复杂度为 O(n)O(n)。
     * 作者：LeetCode-Solution
     * 链接：https://leetcode-cn.com/problems/4sum/solution/si-shu-zhi-he-by-leetcode-solution/
     */
    public List<List<Integer>> fourSum_3(int[] nums, int target) {
        List<List<Integer>> ans = new ArrayList<>();
        int length = nums.length;
        if (length < 4) {
            return ans;
        }
        Arrays.sort(nums);
        for (int i = 0; i < length - 3; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
                break;
            }
            if (nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] < target) {
                continue;
            }
            for (int j = i + 1; j < length - 2; j++) {
                if (j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }
                if (nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
                    break;
                }
                if (nums[i] + nums[j] + nums[length - 2] + nums[length - 1] < target) {
                    continue;
                }
                int left = j + 1, right = length - 1;
                while (left < right) {
                    int sum = nums[i] + nums[j] + nums[left] + nums[right];
                    if (sum == target) {
                        ans.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        while (left < right && nums[left] == nums[left + 1]) {
                            left++;
                        }
                        left++;
                        while (left < right && nums[right] == nums[right - 1]) {
                            right--;
                        }
                        right--;
                    } else if (sum > target) {
                        right--;
                    } else {
                        left++;
                    }
                }
            }
        }
        return ans;
    }
}
